#include "viewer.h"

#include "xfunc.widget.h"
#include "color.widget.h"
#include "misc.h"
#include "tracer.h"
#include "load.thread.h"
#include "session.h"
#include "options.h"
#include "graph.priv.h"
#include "options.page.h"

#include <QtWidgets/QtWidgets>
#include <QOpenGLFunctions_3_3_Core>
#include <cmath>
#include <functional>


#define MAX_SLICES 480

#define PICK_SIZE 4

template<typename Res, typename T, typename... Args>
struct glCallAndCheckHelper {
	T* p;
	Res (T::*f)(Args...);
	const char* file;
	int line;
	glCallAndCheckHelper(T* _p, Res (T::*_f)(Args...), const char* _file, int _line): p{_p}, f{_f}, file{_file}, line{_line} { }
	Res operator()(Args... args) {
		Res r=(p->*f)(args...);
		auto e=p->glGetError();
		if(e!=GL_NO_ERROR)
			throwError(QString{"%1(%2): GL Error %3"}.arg(file).arg(line).arg(e));
		return r;
	}
};

template<typename T, typename... Args>
struct glCallAndCheckHelper<void, T, Args...> {
	T* p;
	void (T::*f)(Args...);
	const char* file;
	int line;
	glCallAndCheckHelper(T* _p, void (T::*_f)(Args...), const char* _file, int _line): p{_p}, f{_f}, file{_file}, line{_line} { }
	void operator()(Args... args) {
		(p->*f)(args...);
		auto e=p->glGetError();
		if(e!=GL_NO_ERROR)
			throwError(QString{"%1(%2): GL Error %3"}.arg(file).arg(line).arg(e));
	}
};

template<typename Res, typename T, typename... Args>
inline glCallAndCheckHelper<Res, T, Args...> glCallAndCheck(Res (T::*func)(Args...), T* ptr, const char* file, int line) {
	return glCallAndCheckHelper<Res, T, Args...>{ptr, func, file, line};
}
#define gl(suffix, ptr) glCallAndCheck(&QOpenGLFunctions_3_3_Core::gl##suffix, ptr, __FILE__, __LINE__)

//#define DEBUG_FPS
#ifdef DEBUG_FPS
void printFPS() {
	static qint64 prevt=0;
	static int prevc=0;
	qint64 nowt=QDateTime::currentMSecsSinceEpoch();
	prevc++;
	if(nowt>prevt+1000) {
		printMessage("FPS ", 1000.0*prevc/(nowt-prevt));
		prevt=nowt;
		prevc=0;
	}

}
#endif
/*
static void debugMatrix(const char* s, const QMatrix4x4& m) {
	fprintf(stderr, "%s: \n", s);
	for(int i=0; i<4; i++) {
		fprintf(stderr, "   ");
		for(int j=0; j<4; j++) {
			fprintf(stderr, "%lf, ", m(i, j));
		}
		fprintf(stderr, "\n");
	}
}
*/

/*
	static void debugBuffer(QOpenGLFunctions_3_3_Core *funcs, GLuint buf) {
	GLint size, immu, flags, usage;
	gl(BindBuffer, funcs)(GL_ARRAY_BUFFER, buf);
	gl(GetBufferParameteriv, funcs)(GL_ARRAY_BUFFER, GL_BUFFER_IMMUTABLE_STORAGE, &immu);
	gl(GetBufferParameteriv, funcs)(GL_ARRAY_BUFFER, GL_BUFFER_SIZE, &size);
	gl(GetBufferParameteriv, funcs)(GL_ARRAY_BUFFER, GL_BUFFER_STORAGE_FLAGS, &flags);
	gl(GetBufferParameteriv, funcs)(GL_ARRAY_BUFFER, GL_BUFFER_USAGE, &usage);
	fprintf(stderr, "Buffer %d: size %d immu %d flags %d usage %d\n", buf, size, immu, flags, usage);
	}
	*/

static const int pickBuffOrder[]={
	40,31,39,41,49,30,32,48,50,22,
	38,42,58,21,23,29,33,47,51,57,
	59,20,24,56,60,13,37,43,67,12,
	14,28,34,46,52,66,68,11,15,19,
	25,55,61,65,69,4,36,44,76,3,
	5,27,35,45,53,75,77,10,16,64,70};

enum Color : int {
	C_CHAN0=0,
	C_CHAN1,
	C_CHAN2,
	C_AXIS,
	C_ALERT,
	C_ATTENTION,
	C_TYPE_0,
	C_TYPE_1,
	C_TYPE_2,
	C_TYPE_3,
	C_TYPE_4,
	C_TYPE_5,
	C_TYPE_6,
	C_TYPE_7,
	C_TYPE_OTHER,
	COLOR_NUM
};

struct {
	Color col;
	QColor def;
	const char* title;
	const char* desc;
} colorData[]={
	{C_CHAN0, QColor{255, 0, 0}, "Channel 1", "Channel 1"},
	{C_CHAN1, QColor{0, 255, 0}, "Channel 2", "Channel 2"},
	{C_CHAN2, QColor{0, 0, 255}, "Channel 3", "Channel 3"},
	{C_AXIS, QColor{0, 255, 255}, "Axis", "Axis lines and labels"},
	{C_ALERT, QColor{255, 0, 0}, "Warning", "Indication of loops or unfinished vertices"},
	{C_ATTENTION, QColor{255, 127, 0}, "Attention", "Current and target position, tracing progress, and candidate path"},
	{C_TYPE_0, QColor{0, 255, 0}, "Color0", "Type 0 (Undefined) or neuron number 0 (modulo 8)"},
	{C_TYPE_1, QColor{0, 0, 255}, "Color1", "Type 1 (Soma) or neuron number 1 (modulo 8)"},
	{C_TYPE_2, QColor{255, 255, 0}, "Color2", "Type 2 (Dendrite) or neuron number 2 (modulo 8)"},
	{C_TYPE_3, QColor{255, 0, 255}, "Color3", "Type 3 (Apical Dendrite) or neuron number 3 (modulo 8)"},
	{C_TYPE_4, QColor{0, 127, 255}, "Color4", "Type 4 (Axon) or neuron number 4 (modulo 8)"},
	{C_TYPE_5, QColor{127, 0, 255}, "Color5", "Type 5 (Mark 1) or neuron number 5 (modulo 8)"},
	{C_TYPE_6, QColor{127, 127, 255}, "Color6", "Type 6 (Mark 2) or neuron number 6 (modulo 8)"},
	{C_TYPE_7, QColor{255, 255, 127}, "Color7", "Type 7 (Mark 3) or neuron number 7 (modulo 8)"},
	{C_TYPE_OTHER, QColor{255, 127, 127}, "ColorX", "Other types or not part of a neuron"}
};

#include "viewer.options.h"

enum ShaderProg {
	// Volume
	P_VOLUME=0,
	// misc
	// Edges
	P_EDGE,
	P_SHADE,
	// Pick
	//XXX P_PICK_MARK,
	// Misc
	P_POINT,
	P_LINE,
	P_MESH,
	P_MARK,
	P_SORT,
	SHADER_PROG_NUM
};

struct {
	ShaderProg prog;
	const char* vert;
	const char* geom;
	const char* frag;
} shaderSrcs[]= {
	{P_VOLUME, ":/shaders/volume.vert", nullptr, ":/shaders/volume.frag"},


	{P_EDGE, ":/shaders/edge.vert", ":/shaders/edge.geom", ":/shaders/edge.frag"},
	{P_SHADE, ":/shaders/shade.vert", ":/shaders/shade.geom", ":/shaders/shade.frag"},


	{P_POINT, ":/shaders/point.vert", ":/shaders/point.geom", ":/shaders/point.frag"},
	{P_LINE, ":/shaders/line.vert", ":/shaders/line.geom", ":/shaders/line.frag"},
	{P_MESH, ":/shaders/mesh.vert", nullptr, ":/shaders/mesh.frag"},
	{P_MARK, ":/shaders/mark.vert", ":/shaders/mark.geom", ":/shaders/mark.frag"},
	{P_SORT, ":/shaders/sort.vert", nullptr, ":/shaders/sort.frag"},
	/*
		//{P_PICK_MARK, ":/shaders/mark.pick.vert", nullptr, ":/shaders/mark.pick.frag"},
		*/
};

struct PointGL {
	GLint p[3]; // position, fixed-point, precision iii.f, in um.
	// 12bytes
	GLushort r; // radius, fixed-point, precision i.f, in um.
	GLshort m; // mark: 0, no mark; >0, different point types; <0, invalid.
	// 16bytes
};

struct PathBuffer {
	GLuint vao;
	GLuint vbo;
	int prev_unused;
};

struct MouseState {
	int xPressed;
	int yPressed;
	int x;
	int y;
	bool moved;
};

struct VertBuffer {
};
struct CubeTexture {
	GLuint tex;
	int refc;
	CubeId cubeId;
	int prev_unused;
};
struct ViewerShared {
	QOpenGLShaderProgram* progs[SHADER_PROG_NUM];
	std::vector<VertBuffer> vertBuffers;
	std::vector<CubeTexture> cubeTextures;
	GLuint vbo_fixed;
	int refc;

	ViewerShared():
		progs{nullptr,},
		vertBuffers{},
		cubeTextures{},
		vbo_fixed{0},
		refc{0}
	{
		CubeTexture texHead;
		texHead.tex=0;
		texHead.refc=0;
		texHead.prev_unused=0;
		cubeTextures.emplace_back(texHead);
	}
	bool initialize(QOpenGLFunctions_3_3_Core* funcs) {
		if(refc) {
			refc++;
			return true;
		}

		for(auto& ss: shaderSrcs) {
			auto tprev=QDateTime::currentDateTime().toMSecsSinceEpoch();
			//
			progs[ss.prog]=new QOpenGLShaderProgram{Tracer::instance()};
			if(!progs[ss.prog])
				throwError("Failed to allocate shader program");
			if(!progs[ss.prog]->create())
				throwError("Failed to create shader program");
			if(!progs[ss.prog]->addShaderFromSourceFile(QOpenGLShader::Vertex, ss.vert))
				throwError("Failed to compile vertex shader: "+progs[ss.prog]->log());
			if(!progs[ss.prog]->addShaderFromSourceFile(QOpenGLShader::Fragment, ss.frag))
				throwError("Failed to compile fragment shader: "+progs[ss.prog]->log());
			if(ss.geom) {
				if(!progs[ss.prog]->addShaderFromSourceFile(QOpenGLShader::Geometry, ss.geom))
					throwError("Failed to compile geom shader: "+progs[ss.prog]->log());
			}
			if(!progs[ss.prog]->link())
				throwError("Failed to link shaders: "+progs[ss.prog]->log());
			auto tnow=QDateTime::currentDateTime().toMSecsSinceEpoch();
			printMessage(ss.vert, " ", ss.frag, " ", tnow-tprev);
		}
		progs[P_SORT]->bind();
		progs[P_SORT]->setUniformValue("tex_edges_depth", 8);
		progs[P_SORT]->setUniformValue("tex_edges_color", 11);
		progs[P_SORT]->setUniformValue("tex_opaque_depth", 12);
		progs[P_SORT]->setUniformValue("tex_opaque_color", 13);
		progs[P_SORT]->setUniformValue("tex_surface_depth", 14);
		progs[P_SORT]->setUniformValue("tex_surface_color", 15);
		progs[P_SORT]->setUniformValue("tex_volume0_depth", 2);
		progs[P_SORT]->setUniformValue("tex_volume0_color", 3);
		progs[P_SORT]->setUniformValue("tex_volume1_depth", 4);
		progs[P_SORT]->setUniformValue("tex_volume1_color", 5);
		progs[P_SORT]->setUniformValue("tex_volume2_depth", 6);
		progs[P_SORT]->setUniformValue("tex_volume2_color", 7);
		progs[P_VOLUME]->bind();
		progs[P_VOLUME]->setUniformValue("tex3d_cube", 1);

		std::vector<GLfloat> fixed_data{
			-1.0f, 1.0f, 0.0f, // 0 box (slice proxy)
				-1.0f, -1.0f, 0.0f,
				1.0f, 1.0f, 0.0f,
				1.0f, -1.0f, 0.0f,
				1.0f, 0, 0, // 4 line
				0, 1.0f, 0,
				-1.0f, 0.0f, 0.0f, // 6 cross
				1.0f, 0.0f, 0.0f,
				0.0f, 1.0f, 0.0f,
				0.0f, -1.0f, 0.0f,
				-1.0f, 0.0f, 0.0f, // 10 open cross
				-.5f, 0.0f, 0.0f,
				1.0f, 0.0f, 0.0f,
				.5f, 0.0f, 0.0f,
				0.0f, -1.0f, 0.0f,
				0.0f, -0.5f, 0.0f,
				0.0f, 1.0f, 0.0f,
				0.0f, 0.5f, 0.0f,

				-.8f, 1.0f, 0.0f, // 18 X
				.8f, -1.0f, 0.0f,
				-1.0f, -1.0f, 0.0f,
				1.0f, 1.0f, 0.0f,
				-.8f, 1.0f, 0.0f, // 22 Y
				0.0f, 0.0f, 0.0f,
				-1.0f, -1.0f, 0.0f,
				1.0f, 1.0f, 0.0f,
				-1.0f, -1.0f, 0.0f, // 26 Z
				1.0f, 1.0f, 0.0f,
				-.8f, 1.0f, 0.0f,
				1.0f, 1.0f, 0.0f,
				.8f, -1.0f, 0.0f,
				-1.0f, -1.0f, 0.0f,
				-1.0f, 1.0f, 0.0f, // 32 err box
				-1.0f, -1.0f, 0.0f,
				1.0f, -1.0f, 0.0f,
				1.0f, 1.0f, 0.0f,
				-1.5f, 1.0f, 0.0f, // 36 err box
				-1.0f, -1.0f, 0.0f,
				1.5f, -1.0f, 0.0f,
				1.0f, 1.0f, 0.0f,
				-.707f, -.707f, 0.0f, // 40 cross
				.707f, .707f, 0.0f,
				-.707f, .707f, 0.0f,
				.707f, -.707f, 0.0f,
				-1.0f, -.707f, 0.0f, // 44 frame
				-1.0f, -1.0f, 0.0f,
				-.707f, -1.0f, 0.0f,
				-1.0f, -1.0f, 0.0f,
				-1.0f, .707f, 0.0f,
				-1.0f, 1.0f, 0.0f,
				-.707f, 1.0f, 0.0f,
				-1.0f, 1.0f, 0.0f,
				1.0f, -.707f, 0.0f,
				1.0f, -1.0f, 0.0f,
				.707f, -1.0f, 0.0f,
				1.0f, -1.0f, 0.0f,
				1.0f, .707f, 0.0f,
				1.0f, 1.0f, 0.0f,
				.707f, 1.0f, 0.0f,
				1.0f, 1.0f, 0.0f,
		};
		fixed_data.resize(300);
		for(int i=0; i<64; i++) {
			float x=cos(i*2*M_PI/64);
			float y=sin(i*2*M_PI/64);
			fixed_data.emplace_back(x);
			fixed_data.emplace_back(y);
			fixed_data.emplace_back(0);
		}
		gl(GenBuffers, funcs)(1, &vbo_fixed);
		gl(BindBuffer, funcs)(GL_ARRAY_BUFFER, vbo_fixed);
		gl(BufferData, funcs)(GL_ARRAY_BUFFER, fixed_data.size()*sizeof(GLfloat), fixed_data.data(), GL_STATIC_DRAW);

		refc++;
		return true;
	}
	bool deinitialize(QOpenGLFunctions_3_3_Core* funcs) {
		if(!refc)
			return false;
		refc--;
		if(refc)
			return true;

		if(vbo_fixed)
			gl(DeleteBuffers, funcs)(1, &vbo_fixed);
		for(auto& prog: progs) {
			if(prog) delete prog;
		}
		for(size_t i=1; i<cubeTextures.size(); i++) {
			gl(DeleteTextures, funcs)(1, &cubeTextures[i].tex);
		}
		return true;
	}
	//

	//
	//...
	//

	~ViewerShared() {
	}

	bool releaseTexture(QOpenGLFunctions_3_3_Core* funcs, int i) {
		if(!cubeTextures[i].refc)
			return false;
		cubeTextures[i].refc--;
		if(cubeTextures[i].refc)
			return true;

		if(cubeTextures[i].tex) {
			gl(DeleteTextures, funcs)(1, &cubeTextures[i].tex);
			cubeTextures[i].tex=0;
		}
		cubeTextures[i].prev_unused=cubeTextures[0].prev_unused;
		cubeTextures[0].prev_unused=i;
		return true;
	}
	int getTexture(QOpenGLFunctions_3_3_Core* funcs, const CubeDataRef& data) {
		for(size_t i=1; i<cubeTextures.size(); i++) {
			if(cubeTextures[i].refc && cubeTextures[i].cubeId==data.cubeId()) {
				cubeTextures[i].refc++;
				return i;
			}
		}
		int i;
		//
		if(cubeTextures[0].prev_unused) {
			i=cubeTextures[0].prev_unused;
			cubeTextures[0].prev_unused=cubeTextures[i].prev_unused;
		} else {
			CubeTexture tex;
			tex.tex=0;
			tex.refc=0;
			cubeTextures.emplace_back(tex);
			i=cubeTextures.size()-1;
		}
		switch(data.type()) {
			case CubeType::U8:
				gl(GenTextures, funcs)(1, &cubeTextures[i].tex);
				gl(ActiveTexture, funcs)(GL_TEXTURE1);
				gl(BindTexture, funcs)(GL_TEXTURE_3D, cubeTextures[i].tex);
				gl(TexParameteri, funcs)(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
				gl(TexParameteri, funcs)(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
				gl(TexParameteri, funcs)(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
				gl(TexParameteri, funcs)(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
				gl(TexParameteri, funcs)(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_BORDER);
				gl(BindTexture, funcs)(GL_TEXTURE_3D, cubeTextures[i].tex);
				printMessage(data.width(), ' ', data.height(), ' ', data.depth(), static_cast<const void*>(data.data()));
				gl(TexImage3D, funcs)(GL_TEXTURE_3D, 0, GL_R8, data.widthAdj(), data.heightAdj(), data.depthAdj(), 0, GL_RED, GL_UNSIGNED_BYTE, data.data());
				break;
			case CubeType::U16:
				gl(GenTextures, funcs)(1, &cubeTextures[i].tex);
				gl(ActiveTexture, funcs)(GL_TEXTURE1);
				gl(BindTexture, funcs)(GL_TEXTURE_3D, cubeTextures[i].tex);
				gl(TexParameteri, funcs)(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
				gl(TexParameteri, funcs)(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
				gl(TexParameteri, funcs)(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
				gl(TexParameteri, funcs)(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
				gl(TexParameteri, funcs)(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_BORDER);
				gl(BindTexture, funcs)(GL_TEXTURE_3D, cubeTextures[i].tex);
				printMessage(data.width(), ' ', data.height(), ' ', data.depth(), static_cast<const void*>(data.data()));
				gl(TexImage3D, funcs)(GL_TEXTURE_3D, 0, GL_R16, data.widthAdj(), data.heightAdj(), data.depthAdj(), 0, GL_RED, GL_UNSIGNED_SHORT, data.data());
				break;
			case CubeType::F32:
				gl(GenTextures, funcs)(1, &cubeTextures[i].tex);
				gl(ActiveTexture, funcs)(GL_TEXTURE1);
				gl(BindTexture, funcs)(GL_TEXTURE_3D, cubeTextures[i].tex);
				gl(TexParameteri, funcs)(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
				gl(TexParameteri, funcs)(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
				gl(TexParameteri, funcs)(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
				gl(TexParameteri, funcs)(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
				gl(TexParameteri, funcs)(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_BORDER);
				gl(BindTexture, funcs)(GL_TEXTURE_3D, cubeTextures[i].tex);
				printMessage(data.width(), ' ', data.height(), ' ', data.depth(), static_cast<const void*>(data.data()));
				gl(TexImage3D, funcs)(GL_TEXTURE_3D, 0, GL_R32F, data.widthAdj(), data.heightAdj(), data.depthAdj(), 0, GL_RED, GL_FLOAT, data.data());
				break;
			default:
				break;
		}
		cubeTextures[i].cubeId=data.cubeId();
		cubeTextures[i].refc++;
		return i;
	}
};

ViewerShared* Viewer::createShared() {
	return new ViewerShared{};
}
void Viewer::destroyShared(ViewerShared* viewerShared) {
	delete viewerShared;
}


struct ViewerPriv {
	Session* session;
	Viewer* viewer;

	ViewerShared* viewerShared;

	Graph graph;

	std::array<Cube, 9> cubes;
	std::array<std::pair<double, double>, 9> xfuncs;
	std::array<int, 9> cube_textures;
	std::array<QMatrix4x4, 9> cube_matrices;
	std::array<double, 9> min_voxel_sizes;
	double min_voxel_size;

	double max_dimension_size;

	QColor colors[COLOR_NUM];
	bool colorMode;

	QVector3D up, rgt;
	float radU;

	Position curPos, tgtPos;
	QVector3D pickA, pickB;
	QMatrix4x4 mView, mrView, mProj, mrProj;

	int scale_factor;
	int fbo_width, fbo_height;

	QOpenGLFunctions_3_3_Core* funcs;
	std::vector<PathBuffer> pathBuffers;
	GLuint vao_fixed;
	GLuint vao_progr, vbo_progr;
	size_t num_progr;

	GLuint fbo_scale, fbotex_scale[2];
	GLuint fbo_opaque, fbotex_opaque[2];
	GLuint fbo_surface, fbotex_surface[2];
	GLuint fbo_edges, fbotex_edges[4];
	// fbo for surfaces
	GLuint fbo_cubes[3], fbotex_cubes[3*2];

	int pbiPath, pbiFixed;
	size_t num_path;

	MouseState mouse;
	int slice_num;
	int slice_shown;
	int slice_delta;
	bool slice_mode;
	bool show_edges;

	struct MeshVert {
		GLint x, y, z;
		GLfloat nx, ny, nz;
	};
	std::vector<MeshVert> meshVerts;
	std::vector<GLuint> meshIdxes;
	GLuint vao_mesh;
	GLuint vbo_mesh[2];

	std::vector<QColor> colormap;
	QTimer* timer;

	ViewerPriv(Session* s, Viewer* v):
		session{s},
		viewer{v}, viewerShared{Tracer::instance()->viewerShared()},
		graph{},
		cube_textures{0,},
		max_dimension_size{0.0},
		colors{}, colorMode{false},
		up{0, -1.0f, 0}, rgt{0, 0, -1.0f}, radU{-1},
		curPos{}, tgtPos{},
		pickA{}, pickB{},
		mView{}, mrView{}, mProj{}, mrProj{},
		scale_factor{0}, fbo_width{0}, fbo_height{0},
		funcs{nullptr},
		pathBuffers{}, vao_fixed{0},
		vao_progr{0}, vbo_progr{0}, num_progr{0},
		fbo_scale{0}, fbotex_scale{0,},
		fbo_opaque{0}, fbotex_opaque{0,},
		fbo_surface{0}, fbotex_surface{0,},
		fbo_edges{0}, fbotex_edges{0},
		fbo_cubes{0,}, fbotex_cubes{0,},
		pbiPath{0}, num_path{0},
		mouse{}, slice_num{800}, slice_shown{800}, slice_delta{0},
		slice_mode{false}, show_edges{true}
	{
		PathBuffer pbHead;
		pbHead.vao=0;
		pbHead.vbo=0;
		pbHead.prev_unused=0;
		pathBuffers.emplace_back(pbHead);

		// XXX color config
		for(int i=0; i<COLOR_NUM; i++) {
			//
			colors[i]=colorData[i].def;
		}
		timer=new QTimer{viewer};
		timer->setInterval(33);
		QObject::connect(timer, &QTimer::timeout, [this]() { this->rotationTimeout(); });
	}

	~ViewerPriv() {
		if(funcs) {
			gl(BindBuffer, funcs)(GL_ARRAY_BUFFER, 0);
			gl(BindVertexArray, funcs)(0);
			for(size_t i=1; i<pathBuffers.size(); i++) {
				gl(DeleteBuffers, funcs)(1, &pathBuffers[i].vbo);
				gl(DeleteVertexArrays, funcs)(1, &pathBuffers[i].vao);
			}
			gl(DeleteBuffers, funcs)(1, &vbo_progr);
			gl(DeleteVertexArrays, funcs)(1, &vao_fixed);
			gl(DeleteVertexArrays, funcs)(1, &vao_progr);

			gl(DeleteBuffers, funcs)(2, vbo_mesh);
			gl(DeleteVertexArrays, funcs)(1, &vao_mesh);

			gl(ActiveTexture, funcs)(GL_TEXTURE0);
			gl(BindTexture, funcs)(GL_TEXTURE_2D, 0);
			gl(DeleteTextures, funcs)(2, fbotex_scale);

			for(int i=0; i<6; i++) {
				gl(ActiveTexture, funcs)(GL_TEXTURE2+i);
				gl(BindTexture, funcs)(GL_TEXTURE_2D, 0);
			}
			gl(DeleteTextures, funcs)(6, fbotex_cubes);

			for(int i=0; i<4; i++) {
				gl(ActiveTexture, funcs)(GL_TEXTURE8+i);
				gl(BindTexture, funcs)(GL_TEXTURE_2D, 0);
			}
			gl(DeleteTextures, funcs)(4, fbotex_edges);

			for(int i=0; i<2; i++) {
				gl(ActiveTexture, funcs)(GL_TEXTURE12+i);
				gl(BindTexture, funcs)(GL_TEXTURE_2D, 0);
			}
			gl(DeleteTextures, funcs)(2, fbotex_opaque);

			for(int i=0; i<2; i++) {
				gl(ActiveTexture, funcs)(GL_TEXTURE14+i);
				gl(BindTexture, funcs)(GL_TEXTURE_2D, 0);
			}
			gl(DeleteTextures, funcs)(2, fbotex_surface);

			gl(BindFramebuffer, funcs)(GL_FRAMEBUFFER, 0);
			gl(DeleteFramebuffers, funcs)(1, &fbo_scale);
			gl(DeleteFramebuffers, funcs)(1, &fbo_opaque);
			gl(DeleteFramebuffers, funcs)(1, &fbo_surface);
			gl(DeleteFramebuffers, funcs)(1, &fbo_edges);
			gl(DeleteFramebuffers, funcs)(3, fbo_cubes);

			viewerShared->deinitialize(funcs);
		}
	}
	void updateVoxelSizes(int i) {
		min_voxel_sizes[i]=qInf();
		for(int j=0; j<3; j++) {
			if(min_voxel_sizes[i]>cubes[i].xform.resolution[j])
				min_voxel_sizes[i]=cubes[i].xform.resolution[j];
			//if(min_voxel_size>cubes[i].xform.resolution[j])
				//min_voxel_size=cubes[i].xform.resolution[j];
		}
	}
	bool setSession(Options* options, Graph gr, const std::array<Cube, 9>& _cubes, const std::array<std::pair<double, double>, 9>& _xfuncs, const std::array<double, 6>& bbox, double mvol) {
		graph=gr;
		cubes=_cubes;
		xfuncs=_xfuncs;

		max_dimension_size=0.0;
		if(bbox[1]-bbox[0]>max_dimension_size)
			max_dimension_size=bbox[1]-bbox[0];
		if(bbox[3]-bbox[2]>max_dimension_size)
			max_dimension_size=bbox[3]-bbox[2];
		if(bbox[5]-bbox[4]>max_dimension_size)
			max_dimension_size=bbox[5]-bbox[4];

		min_voxel_size=mvol;
		for(int i=0; i<9; i++) {
			if(cubes[i].data) {
				updateCubeMatrix(i);
				updateVoxelSizes(i);
			}
		}

		double x, y, z;
		if(options->getXyz("viewer.direction.up", &x, &y, &z)) {
			up.setX(x); up.setY(y); up.setZ(z);
		} else {
			up.setX(0); up.setY(-1); up.setZ(0);
		}
		if(options->getXyz("viewer.direction.right", &x, &y, &z)) {
			rgt.setX(x); rgt.setY(y); rgt.setZ(z);
		} else {
			rgt.setX(0); rgt.setY(0); rgt.setZ(-1);
		}
		if(options->getDouble("viewer.zoom", &x)) {
			radU=x;
		} else {
			radU=max_dimension_size/2;
		}

		ViewerGeneralOptions::instance()->config(this, options);
		ViewerColorOptions::instance()->config(this, options);
		ViewerVolumeOptions::instance()->config(this, options);
		auto dpr=viewer->devicePixelRatio();
		auto w=viewer->width();
		auto h=viewer->height();
		int ww=dpr*w;
		int hh=dpr*h;

		viewer->makeCurrent();
		resizeFrameBuffers(ww, hh);
		initGraphPbi();
		gl(BindBuffer, funcs)(GL_ARRAY_BUFFER, pathBuffers[pbiFixed].vbo);
		auto ptr=static_cast<PointGL*>(funcs->glMapBufferRange(GL_ARRAY_BUFFER, (2)*sizeof(PointGL), 9*24*sizeof(PointGL), GL_MAP_WRITE_BIT|GL_MAP_INVALIDATE_RANGE_BIT));
		for(int ci=0; ci<9; ci++) {
			if(cubes[ci].data) {
				setAxes(cubes[ci], &ptr[ci*24]);
			}
		}
		gl(UnmapBuffer, funcs)(GL_ARRAY_BUFFER);
		viewer->doneCurrent();

		viewer->setCursor(Qt::CrossCursor);
		return true;
	}
	bool updateBoundingBox(const std::array<double, 6>& bbox) {
		if(bbox[1]-bbox[0]>max_dimension_size)
			max_dimension_size=bbox[1]-bbox[0];
		if(bbox[3]-bbox[2]>max_dimension_size)
			max_dimension_size=bbox[3]-bbox[2];
		if(bbox[5]-bbox[4]>max_dimension_size)
			max_dimension_size=bbox[5]-bbox[4];
		return true;
	}
	void setAxes(const Cube& c, PointGL* ptr) {
		int64_t size[3]={c.data.width(), c.data.height(), c.data.depth()};
		int32_t p0[3]={c.data.cubeId().x0, c.data.cubeId().y0, c.data.cubeId().z0};
		for(int i=0; i<3; i++) {
			printMessage(size[i]);
		}
		for(int dir=0; dir<3; dir++) {
			for(int i=0; i<2; i++) {
				for(int j=0; j<2; j++) {
					for(int k=0; k<2; k++) {
						double pp[3];
						for(int l=0; l<3; l++)
							pp[l]=c.xform.origin[l]
								+c.xform.direction[l+dir*3]*(k*size[dir]+p0[dir])
								+c.xform.direction[l+(dir+1)%3*3]*(j*size[(dir+1)%3]+p0[(dir+1)%3])
								+c.xform.direction[l+(dir+2)%3*3]*(i*size[(dir+2)%3]+p0[(dir+2)%3]);
						Point p(pp[0], pp[1], pp[2]);
						ptr[dir*8+i*4+j*2+k].p[0]=p._x;
						ptr[dir*8+i*4+j*2+k].p[1]=p._y;
						ptr[dir*8+i*4+j*2+k].p[2]=p._z;
					}
				}
			}
		}
	}
	void updateXfunc(int ch, const std::pair<double, double>& xfunc) {
		xfuncs[ch]=xfunc;
	}

	bool pickPoint(int x, int y) {
		int pick_size;
		int fbx, fby;
		if(scale_factor<0) {
			pick_size=PICK_SIZE*(1-scale_factor);
			fbx=x*(1-scale_factor)+(1-scale_factor)/2;
			fby=y*(1-scale_factor)+(1-scale_factor)/2;
		} else {
			pick_size=PICK_SIZE;
			fbx=x/(1+scale_factor);
			fby=y/(1+scale_factor);
		}
		//
		//
		float px=(fbx*2+1.0)/fbo_width-1;
		float py=1-(fby*2+1.0)/fbo_height;
		if(slice_mode) {
			float depth=-1.0*slice_delta/slice_num;
			QVector3D lp=mrView*mrProj*QVector3D(px, py, depth);
			Point p=toGlobal(lp);
			session->pickPosition(Edge{}, 0, &p);
			return true;
		}

		viewer->makeCurrent();
		if(fbx-pick_size>=0 && fby-pick_size>=0
				&& fby+pick_size<fbo_height
				&& fbx+pick_size<fbo_width) {
			int pickdr2=pick_size*pick_size+1;
			int pickI=-1;
			std::vector<GLuint> bufIdx((2*pick_size+1)*(2*pick_size+1));
			gl(BindFramebuffer, funcs)(GL_READ_FRAMEBUFFER, fbo_edges);
			gl(ReadBuffer, funcs)(GL_COLOR_ATTACHMENT0);
			gl(ReadPixels, funcs)(fbx-pick_size, fbo_height-1-fby-pick_size, pick_size*2, pick_size*2, GL_RED_INTEGER, GL_UNSIGNED_INT, &bufIdx[0]);

			for(int dy=-pick_size; dy<=pick_size; dy++) {
				for(int dx=-pick_size; dx<=pick_size; dx++) {
					int i=dx+pick_size+(dy+pick_size)*(2*pick_size+1);
					if(bufIdx[i]>0) {
						auto dr2=dx*dx+dy*dy;
						if(dr2<pickdr2) {
							pickI=i;
							pickdr2=dr2;
						}
					}
				}
			}
			//
			//
			//

			size_t pickIdx=0;

			if(pickI>=0) {
				pickIdx=bufIdx[pickI];
				gl(ReadBuffer, funcs)(GL_COLOR_ATTACHMENT1);
				std::vector<GLint> bufPos((2*pick_size+1)*(2*pick_size+1));
				gl(ReadPixels, funcs)(fbx-pick_size, fbo_height-1-fby-pick_size, pick_size*2, pick_size*2, GL_RED_INTEGER, GL_INT, &bufPos[0]);
				viewer->doneCurrent();

				auto e=graph.edges()[pickIdx-1];
				size_t pickPos=bufPos[pickI];

				if(pickPos>e.points().size()-1) pickPos=e.points().size()-1;
				//fprintf(stderr, " pick %d [%d, %d]\n", pickPos, 0, e.points().size()-1);
				const auto& p=e.points()[pickPos];
				const auto& p0=e.points()[0];
				const auto& p1=e.points()[e.points().size()-1];
				if(pickPos==0) {
					session->pickPosition(e, 0, &p0);
					return true;
				}
				if(pickPos==e.points().size()-1) {
					session->pickPosition(e, e.points().size()-1, &p1);
					return true;
				}
				if(p.distTo(p0)<p0.r()) {
					session->pickPosition(e, 0, &p0);
					return true;
				}
				if(p.distTo(p1)<p1.r()) {
					session->pickPosition(e, e.points().size()-1, &p1);
					return true;
				}
				session->pickPosition(e, pickPos, &p);
				return true;
			}
		}

		QVector3D a(px, py, -1);
		QVector3D b(px, py, 1);
		a=mrView*mrProj*a;
		b=mrView*mrProj*b;
		QVector3D c=a-b;
		auto lenc=c.length();
		c=c/lenc;
		QVector3D selC=pickA-pickB;
		selC.normalize();
		QVector3D d=QVector3D::crossProduct(c, selC);

		QMatrix4x4 mat{};
		mat.setColumn(0, QVector4D(c, 0));
		mat.setColumn(1, QVector4D(-selC, 0));
		mat.setColumn(2, QVector4D(d, 0));
		QMatrix4x4 matr=mat.inverted();
		QVector3D s=matr*(pickB-b);
		//printMessage("pickpoint %1 %2 %3", d.length(), s.x(), lenc);
		if(d.length()<0.1 || s.x()<0 || s.x()>lenc) {
			pickA=a;
			pickB=b;
			gl(BindBuffer, funcs)(GL_ARRAY_BUFFER, pathBuffers[pbiFixed].vbo);
			auto ptr=static_cast<PointGL*>(funcs->glMapBufferRange(GL_ARRAY_BUFFER, 0*sizeof(PointGL), 2*sizeof(PointGL), GL_MAP_WRITE_BIT|GL_MAP_INVALIDATE_RANGE_BIT));
			auto pa=toGlobal(pickA);
			auto pb=toGlobal(pickB);
			ptr[0].p[0]=pa._x;
			ptr[0].p[1]=pa._y;
			ptr[0].p[2]=pa._z;
			ptr[1].p[0]=pb._x;
			ptr[1].p[1]=pb._y;
			ptr[1].p[2]=pb._z;
			gl(UnmapBuffer, funcs)(GL_ARRAY_BUFFER);
			viewer->doneCurrent();
			return false;
		}
		QVector3D t=b+s.x()*c;
		Point gt=toGlobal(t);
		pickA=pickB=QVector3D();
		session->pickPosition(Edge{}, 0, &gt);
		viewer->doneCurrent();
		return true;
	}

	QVector3D toLocal(const Point& p) const {
		Point d;
		d._x=p._x-curPos.point._x;
		d._y=p._y-curPos.point._y;
		d._z=p._z-curPos.point._z;
		return QVector3D(d.x(), d.y(), d.z());
	}
	Point toGlobal(const QVector3D& p) const {
		auto x=p.x()+curPos.point.x();
		auto y=p.y()+curPos.point.y();
		auto z=p.z()+curPos.point.z();
		return Point{x, y, z};
	}
	void updateMVPMatrices() { // Depends on: *rgt, *radU, *up, *fbo_height, *fbo_width
		mView.setToIdentity();
		mView.lookAt(rgt*radU, QVector3D{0, 0, 0}, up);
		mrView=mView.inverted();

		float vspan=radU*fbo_height/fbo_width;
		mProj.setToIdentity();
		mProj.ortho(-radU, radU, -vspan, vspan, 0.5*radU, 1.5*radU);
		mrProj=mProj.inverted();
	}
	void updateCubeMatrix(int ch) { // Depends on: *curPos, cube.xform, cube.data
		std::array<double, 3> p0{curPos.point.x(), curPos.point.y(), curPos.point.z()};
		for(int i=0; i<3; i++)
			p0[i]-=cubes[ch].xform.origin[i];
		std::array<int32_t, 3> p1{-cubes[ch].data.cubeId().x0, -cubes[ch].data.cubeId().y0, -cubes[ch].data.cubeId().z0};
		std::array<int64_t, 3> size{cubes[ch].data.widthAdj(), cubes[ch].data.heightAdj(), cubes[ch].data.depthAdj()};
		for(int i=0; i<4; i++)
			cube_matrices[ch](3, i)=i<3?0:1;
		auto& rdir=cubes[ch].xform.direction_inv;
		for(int i=0; i<3; i++) {
			double v=p1[i];
			for(int j=0; j<3; j++) {
				cube_matrices[ch](i, j)=rdir[i+j*3]/size[i];
				v+=rdir[i+j*3]*p0[j];
			}
			cube_matrices[ch](i, 3)=v/size[i];
		}
		for(int i=0; i<4; i++) {
			for(int j=0; j<4; j++) {
				printMessage(i, j, ": ", cube_matrices[ch](i,j));
			}
		}
	}

	void initializeGL() {
		funcs=viewer->context()->versionFunctions<QOpenGLFunctions_3_3_Core>();
		if(!funcs) {
			throwError("Failed to obtain OpenGL function pointers.");
			auto funcs=viewer->context()->functions();
			throwError("OpenGL API 3.3 not supported. (", funcs->glGetString(GL_VERSION), ")");
		}

		viewerShared->initialize(funcs);

		gl(BindBuffer, funcs)(GL_ARRAY_BUFFER, viewerShared->vbo_fixed);
		gl(GenVertexArrays, funcs)(1, &vao_fixed);
		gl(BindVertexArray, funcs)(vao_fixed);
		gl(EnableVertexAttribArray, funcs)(0);
		gl(VertexAttribPointer, funcs)(0, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*3, 0);

		gl(GenVertexArrays, funcs)(1, &vao_progr);
		gl(BindVertexArray, funcs)(vao_progr);
		gl(GenBuffers, funcs)(1, &vbo_progr);
		gl(BindBuffer, funcs)(GL_ARRAY_BUFFER, vbo_progr);
		gl(EnableVertexAttribArray, funcs)(0);
		gl(VertexAttribIPointer, funcs)(0, 3, GL_INT, sizeof(GLint)*3, nullptr);

		auto dpr=viewer->devicePixelRatio();
		if(scale_factor<0) {
			fbo_width=viewer->width()*dpr*(1-scale_factor);
			fbo_height=viewer->height()*dpr*(1-scale_factor);
		} else {
			fbo_width=(viewer->width()*dpr+scale_factor)/(1+scale_factor);
			fbo_height=(viewer->height()*dpr+scale_factor)/(1+scale_factor);
		}

		gl(GenFramebuffers, funcs)(1, &fbo_opaque);
		gl(GenTextures, funcs)(2, fbotex_opaque);
		gl(BindFramebuffer, funcs)(GL_FRAMEBUFFER, fbo_opaque);
		gl(ActiveTexture, funcs)(GL_TEXTURE12);
		gl(BindTexture, funcs)(GL_TEXTURE_2D, fbotex_opaque[0]);
		gl(TexImage2D, funcs)(GL_TEXTURE_2D, 0, GL_DEPTH_STENCIL, fbo_width, fbo_height, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, nullptr);
		gl(TexParameteri, funcs)(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		gl(TexParameteri, funcs)(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		gl(FramebufferTexture2D, funcs)(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, fbotex_opaque[0], 0);
		gl(ActiveTexture, funcs)(GL_TEXTURE13);
		gl(BindTexture, funcs)(GL_TEXTURE_2D, fbotex_opaque[1]);
		gl(TexImage2D, funcs)(GL_TEXTURE_2D, 0, GL_RGBA8, fbo_width, fbo_height, 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, nullptr);
		gl(TexParameteri, funcs)(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		gl(TexParameteri, funcs)(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		gl(FramebufferTexture2D, funcs)(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fbotex_opaque[1], 0);
		gl(DrawBuffer, funcs)(GL_COLOR_ATTACHMENT0);
		auto check_fbo=funcs->glCheckFramebufferStatus(GL_FRAMEBUFFER);
		if(check_fbo!=GL_FRAMEBUFFER_COMPLETE) {
			throwError("Failed to create framebuffer fbo_opaque: %1", check_fbo);
		}

		gl(GenFramebuffers, funcs)(1, &fbo_surface);
		gl(GenTextures, funcs)(2, fbotex_surface);
		gl(BindFramebuffer, funcs)(GL_FRAMEBUFFER, fbo_surface);
		gl(ActiveTexture, funcs)(GL_TEXTURE14);
		gl(BindTexture, funcs)(GL_TEXTURE_2D, fbotex_surface[0]);
		gl(TexImage2D, funcs)(GL_TEXTURE_2D, 0, GL_DEPTH_STENCIL, fbo_width, fbo_height, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, nullptr);
		gl(TexParameteri, funcs)(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		gl(TexParameteri, funcs)(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		gl(FramebufferTexture2D, funcs)(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, fbotex_surface[0], 0);
		gl(ActiveTexture, funcs)(GL_TEXTURE15);
		gl(BindTexture, funcs)(GL_TEXTURE_2D, fbotex_surface[1]);
		gl(TexImage2D, funcs)(GL_TEXTURE_2D, 0, GL_RGBA8, fbo_width, fbo_height, 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, nullptr);
		gl(TexParameteri, funcs)(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		gl(TexParameteri, funcs)(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		gl(FramebufferTexture2D, funcs)(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fbotex_surface[1], 0);
		gl(DrawBuffer, funcs)(GL_COLOR_ATTACHMENT0);
		check_fbo=funcs->glCheckFramebufferStatus(GL_FRAMEBUFFER);
		if(check_fbo!=GL_FRAMEBUFFER_COMPLETE) {
			throwError("Failed to create framebuffer fbo_surface: %1", check_fbo);
		}

		gl(GenFramebuffers, funcs)(1, &fbo_edges);
		gl(GenTextures, funcs)(4, fbotex_edges);
		gl(BindFramebuffer, funcs)(GL_FRAMEBUFFER, fbo_edges);
		gl(ActiveTexture, funcs)(GL_TEXTURE8);
		gl(BindTexture, funcs)(GL_TEXTURE_2D, fbotex_edges[0]);
		gl(TexImage2D, funcs)(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, fbo_width, fbo_height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);
		gl(TexParameteri, funcs)(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		gl(TexParameteri, funcs)(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		gl(FramebufferTexture2D, funcs)(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, fbotex_edges[0], 0);
		gl(ActiveTexture, funcs)(GL_TEXTURE9);
		gl(BindTexture, funcs)(GL_TEXTURE_2D, fbotex_edges[1]);
		gl(TexImage2D, funcs)(GL_TEXTURE_2D, 0, GL_R32I, fbo_width, fbo_height, 0, GL_RED_INTEGER, GL_UNSIGNED_INT, nullptr);
		gl(TexParameteri, funcs)(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		gl(TexParameteri, funcs)(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		gl(FramebufferTexture2D, funcs)(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fbotex_edges[1], 0);
		gl(ActiveTexture, funcs)(GL_TEXTURE10);
		gl(BindTexture, funcs)(GL_TEXTURE_2D, fbotex_edges[2]);
		gl(TexImage2D, funcs)(GL_TEXTURE_2D, 0, GL_R32I, fbo_width, fbo_height, 0, GL_RED_INTEGER, GL_INT, nullptr);
		gl(TexParameteri, funcs)(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		gl(TexParameteri, funcs)(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		gl(FramebufferTexture2D, funcs)(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, fbotex_edges[2], 0);
		gl(ActiveTexture, funcs)(GL_TEXTURE11);
		gl(BindTexture, funcs)(GL_TEXTURE_2D, fbotex_edges[3]);
		gl(TexImage2D, funcs)(GL_TEXTURE_2D, 0, GL_RGBA8, fbo_width, fbo_height, 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, nullptr);
		gl(TexParameteri, funcs)(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		gl(TexParameteri, funcs)(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		gl(FramebufferTexture2D, funcs)(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_TEXTURE_2D, fbotex_edges[3], 0);
		GLenum bufs[]={GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2};
		gl(DrawBuffers, funcs)(3, bufs);
		check_fbo=funcs->glCheckFramebufferStatus(GL_FRAMEBUFFER);
		if(check_fbo!=GL_FRAMEBUFFER_COMPLETE) {
			throwError("Failed to create framebuffer fbo_edges %1", check_fbo);
		}

		gl(GenFramebuffers, funcs)(3, fbo_cubes);
		gl(GenTextures, funcs)(6, fbotex_cubes);
		for(int i=0; i<3; i++) {
			gl(BindFramebuffer, funcs)(GL_FRAMEBUFFER, fbo_cubes[i]);
			gl(ActiveTexture, funcs)(GL_TEXTURE2+i*2+0);
			gl(BindTexture, funcs)(GL_TEXTURE_2D, fbotex_cubes[2*i+0]);
			gl(TexImage2D, funcs)(GL_TEXTURE_2D, 0, GL_DEPTH_STENCIL, fbo_width, fbo_height, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, nullptr);
			gl(TexParameteri, funcs)(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
			gl(TexParameteri, funcs)(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
			gl(FramebufferTexture2D, funcs)(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, fbotex_cubes[2*i+0], 0);
			gl(ActiveTexture, funcs)(GL_TEXTURE2+i*2+1);
			gl(BindTexture, funcs)(GL_TEXTURE_2D, fbotex_cubes[2*i+1]);
			gl(TexImage2D, funcs)(GL_TEXTURE_2D, 0, GL_RGBA8, fbo_width, fbo_height, 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, nullptr);
			gl(TexParameteri, funcs)(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
			gl(TexParameteri, funcs)(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
			gl(FramebufferTexture2D, funcs)(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fbotex_cubes[2*i+1], 0);
			gl(DrawBuffer, funcs)(GL_COLOR_ATTACHMENT0);
			check_fbo=funcs->glCheckFramebufferStatus(GL_FRAMEBUFFER);
			if(check_fbo!=GL_FRAMEBUFFER_COMPLETE) {
				throwError("Failed to create framebuffer fbo_cubes: %1", check_fbo);
			}
		}

		gl(GenFramebuffers, funcs)(1, &fbo_scale);
		gl(GenTextures, funcs)(2, fbotex_scale);
		gl(BindFramebuffer, funcs)(GL_FRAMEBUFFER, fbo_scale);
		gl(ActiveTexture, funcs)(GL_TEXTURE0);
		gl(BindTexture, funcs)(GL_TEXTURE_2D, fbotex_scale[0]);
		gl(TexImage2D, funcs)(GL_TEXTURE_2D, 0, GL_DEPTH_STENCIL, fbo_width, fbo_height, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, nullptr);
		gl(FramebufferTexture2D, funcs)(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, fbotex_scale[0], 0);
		gl(BindTexture, funcs)(GL_TEXTURE_2D, fbotex_scale[1]);
		gl(TexImage2D, funcs)(GL_TEXTURE_2D, 0, GL_RGBA8, fbo_width, fbo_height, 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, nullptr);
		gl(FramebufferTexture2D, funcs)(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fbotex_scale[1], 0);
		gl(DrawBuffer, funcs)(GL_COLOR_ATTACHMENT0);
		check_fbo=funcs->glCheckFramebufferStatus(GL_FRAMEBUFFER);
		if(check_fbo!=GL_FRAMEBUFFER_COMPLETE) {
			throwError("Failed to create framebuffer fbo_scale: %1", check_fbo);
		}

		/*
			GLint v;
			gl(GetRenderbufferParameteriv, funcs)(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &v);
			fprintf(stderr, "w %d\n", v);
			gl(GetRenderbufferParameteriv, funcs)(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &v);
			fprintf(stderr, "h %d\n", v);
			gl(GetRenderbufferParameteriv, funcs)(GL_RENDERBUFFER, GL_RENDERBUFFER_INTERNAL_FORMAT, &v);
			fprintf(stderr, "f %d\n", v);
			gl(GetRenderbufferParameteriv, funcs)(GL_RENDERBUFFER, GL_RENDERBUFFER_SAMPLES, &v);
			fprintf(stderr, "s %d\n", v);
			gl(GetRenderbufferParameteriv, funcs)(GL_RENDERBUFFER, GL_RENDERBUFFER_RED_SIZE, &v);
			fprintf(stderr, "r %d\n", v);
			*/

		gl(ClearColor, funcs)(0.0, 0.0, 0.0, 1.0);
		gl(ClearDepth, funcs)(1.0);
		gl(Enable, funcs)(GL_DEPTH_TEST);
		gl(Enable, funcs)(GL_BLEND);
		gl(BlendFuncSeparate, funcs)(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

		pbiFixed=createPbi();
		pbiPath=createPbi();
		gl(BindBuffer, funcs)(GL_ARRAY_BUFFER, pathBuffers[pbiFixed].vbo);
		gl(BufferData, funcs)(GL_ARRAY_BUFFER, (9*24+2)*sizeof(PointGL), nullptr, GL_STATIC_DRAW);


		/*
		meshVerts.push_back(MeshVert{5000*256, 0, 0, 0.0, 0.0, 0.0});
		meshVerts.push_back(MeshVert{0, 5000*256, 0, 0.0, 0.0, 0.0});
		meshVerts.push_back(MeshVert{0, 0, 5000*256, 0.0, 0.0, 0.0});
		meshIdxes.push_back(0);
		meshIdxes.push_back(2);
		meshIdxes.push_back(1);
		*/

		gl(GenVertexArrays, funcs)(1, &vao_mesh);
		gl(BindVertexArray, funcs)(vao_mesh);
		gl(GenBuffers, funcs)(2, vbo_mesh);
		gl(BindBuffer, funcs)(GL_ARRAY_BUFFER, vbo_mesh[0]);
		gl(EnableVertexAttribArray, funcs)(0);
		gl(VertexAttribIPointer, funcs)(0, 3, GL_INT, sizeof(MeshVert), 0);
		gl(EnableVertexAttribArray, funcs)(1);
		gl(VertexAttribPointer, funcs)(1, 3, GL_FLOAT, GL_FALSE, sizeof(MeshVert), static_cast<char*>(nullptr)+offsetof(MeshVert, nx));
		gl(BufferData, funcs)(GL_ARRAY_BUFFER, sizeof(MeshVert)*meshVerts.size(), &meshVerts[0], GL_STATIC_DRAW);

		gl(BindBuffer, funcs)(GL_ELEMENT_ARRAY_BUFFER, vbo_mesh[1]);
		gl(BufferData, funcs)(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLuint)*meshIdxes.size(), &meshIdxes[0], GL_STATIC_DRAW);
		//gl(EnableVertexAttribArray, funcs)(2);
		//gl(VertexAttribIPointer, funcs)(2, 1, GL_UNSIGNED_SHORT, sizeof(MeshVert), static_cast<char*>(nullptr)+offsetof(MeshVert, id));
		//gl(EnableVertexAttribArray, funcs)(3);
		//gl(VertexAttribIPointer, funcs)(3, 1, GL_UNSIGNED_SHORT, sizeof(MeshVert), static_cast<char*>(nullptr)+offsetof(MeshVert, par));
		//
	}
	std::vector<PointGL> pathToPathGL(const std::vector<Point>& points) {
		std::vector<PointGL> pathGL(points.size()+1);
		for(size_t i=0; i<points.size(); i++) {
			auto& p=points[i];
			pathGL[i].p[0]=p._x;
			pathGL[i].p[1]=p._y;
			pathGL[i].p[2]=p._z;
			pathGL[i].r=p._r;
			pathGL[i].m=p.m;
		}
		pathGL.back().p[0]=points.back()._x;
		pathGL.back().p[1]=points.back()._y;
		pathGL.back().p[2]=points.back()._z;
		pathGL.back().r=0;
		pathGL.back().m=0;
		return pathGL;
	}
	void initGraphPbi() {
		for(auto e: graph.edges()) {
			auto ep=EdgePriv::get(e);
			if(ep->vaoi<=0) {
				auto pathGL=pathToPathGL(ep->points);
				int vaoi=createPbi();
				gl(BindBuffer, funcs)(GL_ARRAY_BUFFER, pathBuffers[vaoi].vbo);
				gl(BufferData, funcs)(GL_ARRAY_BUFFER, sizeof(PointGL)*pathGL.size(), pathGL.data(), GL_STATIC_DRAW);
				ep->vaoi=vaoi;
			}
		}
	}
	int createPbi() {
		if(pathBuffers[0].prev_unused) {
			auto i=pathBuffers[0].prev_unused;
			pathBuffers[0].prev_unused=pathBuffers[i].prev_unused;
			return i;
		}
		PathBuffer pbItem;
		gl(GenVertexArrays, funcs)(1, &pbItem.vao);
		gl(BindVertexArray, funcs)(pbItem.vao);
		gl(GenBuffers, funcs)(1, &pbItem.vbo);
		gl(BindBuffer, funcs)(GL_ARRAY_BUFFER, pbItem.vbo);

		gl(EnableVertexAttribArray, funcs)(0);
		gl(VertexAttribIPointer, funcs)(0, 3, GL_INT, sizeof(PointGL), static_cast<int8_t*>(nullptr)+offsetof(PointGL, p));

		gl(EnableVertexAttribArray, funcs)(1);
		gl(VertexAttribIPointer, funcs)(1, 1, GL_UNSIGNED_SHORT, sizeof(PointGL), static_cast<int8_t*>(nullptr)+offsetof(PointGL, r));

		gl(EnableVertexAttribArray, funcs)(2);
		gl(VertexAttribIPointer, funcs)(2, 1, GL_SHORT, sizeof(PointGL), static_cast<int8_t*>(nullptr)+offsetof(PointGL, m));

		pathBuffers.emplace_back(pbItem);
		return pathBuffers.size()-1;
	}
	void freePbi(int i) {
		pathBuffers[i].prev_unused=pathBuffers[0].prev_unused;
		pathBuffers[0].prev_unused=i;
	}

	void zoomInOut(float d) {
		if(slice_mode) {
			d=d/radU;
			if(d>0 && d<1) d=1;
			if(d<0 && d>-1) d=-1;
			slice_delta+=d;
			if(slice_delta<-slice_num) slice_delta=-slice_num;
			if(slice_delta>slice_num) slice_delta=slice_num;
		} else {
			radU=radU*pow(0.96, d);
			if(radU>2*max_dimension_size) {
				radU=2*max_dimension_size;
			} else if(radU<5*min_voxel_size) {
				radU=5*min_voxel_size;
			}
			updateMVPMatrices();
		}
		viewer->update();
	}

	void resizeFrameBuffers(int ww, int hh) {
		int width, height;
		if(scale_factor<0) {
			width=ww*(1-scale_factor);
			height=hh*(1-scale_factor);
		} else {
			width=(ww+scale_factor)/(1+scale_factor);
			height=(hh+scale_factor)/(1+scale_factor);
		}

		if(width!=fbo_width || height!=fbo_height) {
			fbo_width=width;
			fbo_height=height;

			//gl(ActiveTexture, funcs)(GL_TEXTURE0);
			gl(BindTexture, funcs)(GL_TEXTURE_2D, fbotex_scale[0]);
			gl(TexImage2D, funcs)(GL_TEXTURE_2D, 0, GL_DEPTH_STENCIL, fbo_width, fbo_height, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, nullptr);
			gl(BindTexture, funcs)(GL_TEXTURE_2D, fbotex_scale[1]);
			gl(TexImage2D, funcs)(GL_TEXTURE_2D, 0, GL_RGBA8, fbo_width, fbo_height, 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, nullptr);

			gl(BindTexture, funcs)(GL_TEXTURE_2D, fbotex_opaque[0]);
			gl(TexImage2D, funcs)(GL_TEXTURE_2D, 0, GL_DEPTH_STENCIL, fbo_width, fbo_height, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, nullptr);
			gl(BindTexture, funcs)(GL_TEXTURE_2D, fbotex_opaque[1]);
			gl(TexImage2D, funcs)(GL_TEXTURE_2D, 0, GL_RGBA8, fbo_width, fbo_height, 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, nullptr);

			gl(BindTexture, funcs)(GL_TEXTURE_2D, fbotex_surface[0]);
			gl(TexImage2D, funcs)(GL_TEXTURE_2D, 0, GL_DEPTH_STENCIL, fbo_width, fbo_height, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, nullptr);
			gl(BindTexture, funcs)(GL_TEXTURE_2D, fbotex_surface[1]);
			gl(TexImage2D, funcs)(GL_TEXTURE_2D, 0, GL_RGBA8, fbo_width, fbo_height, 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, nullptr);

			gl(BindTexture, funcs)(GL_TEXTURE_2D, fbotex_edges[0]);
			gl(TexImage2D, funcs)(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, fbo_width, fbo_height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);
			gl(BindTexture, funcs)(GL_TEXTURE_2D, fbotex_edges[1]);
			gl(TexImage2D, funcs)(GL_TEXTURE_2D, 0, GL_R32I, fbo_width, fbo_height, 0, GL_RED_INTEGER, GL_UNSIGNED_INT, nullptr);
			gl(BindTexture, funcs)(GL_TEXTURE_2D, fbotex_edges[2]);
			gl(TexImage2D, funcs)(GL_TEXTURE_2D, 0, GL_R32I, fbo_width, fbo_height, 0, GL_RED_INTEGER, GL_INT, nullptr);
			gl(BindTexture, funcs)(GL_TEXTURE_2D, fbotex_edges[3]);
			gl(TexImage2D, funcs)(GL_TEXTURE_2D, 0, GL_RGBA8, fbo_width, fbo_height, 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, nullptr);

			for(int i=0; i<3; i++) {
				gl(BindTexture, funcs)(GL_TEXTURE_2D, fbotex_cubes[2*i+0]);
				gl(TexImage2D, funcs)(GL_TEXTURE_2D, 0, GL_DEPTH_STENCIL, fbo_width, fbo_height, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, nullptr);
				gl(BindTexture, funcs)(GL_TEXTURE_2D, fbotex_cubes[2*i+1]);
				gl(TexImage2D, funcs)(GL_TEXTURE_2D, 0, GL_RGBA8, fbo_width, fbo_height, 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, nullptr);
			}
			updateMVPMatrices();
		}
	}

	void paintEdge() {
		gl(BindFramebuffer, funcs)(GL_FRAMEBUFFER, fbo_edges);
		gl(Viewport, funcs)(0, 0, fbo_width, fbo_height);
		gl(Clear, funcs)(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT|GL_STENCIL_BUFFER_BIT);
		if(!show_edges)
			return;

		float umpp;
		if(scale_factor<0) {
			umpp=radU*(1-scale_factor)/fbo_width;
		} else {
			umpp=radU/fbo_width;
		}

		gl(Disable, funcs)(GL_BLEND);
		viewerShared->progs[P_EDGE]->bind();
		auto locp=viewerShared->progs[P_EDGE]->uniformLocation("p0int");
		auto& p=curPos.point;
		gl(Uniform3i, funcs)(locp, p._x, p._y, p._z);
		viewerShared->progs[P_EDGE]->setUniformValue("mView", mView);
		viewerShared->progs[P_EDGE]->setUniformValue("mProj", mProj);
		viewerShared->progs[P_EDGE]->setUniformValue("color_edge[1]", colors[C_TYPE_5]);
		viewerShared->progs[P_EDGE]->setUniformValue("color_edge[2]", colors[C_TYPE_6]);
		viewerShared->progs[P_EDGE]->setUniformValue("color_edge[3]", colors[C_TYPE_7]);
		viewerShared->progs[P_EDGE]->setUniformValue("color_edge[4]", colors[C_TYPE_OTHER]);
		auto idx_loc=viewerShared->progs[P_EDGE]->uniformLocation("idx");
		if(num_path) {
			viewerShared->progs[P_EDGE]->setUniformValue("umpp", umpp);
			viewerShared->progs[P_EDGE]->setUniformValue("color_edge[0]", colors[C_ATTENTION]);
			gl(Uniform1ui, funcs)(idx_loc, static_cast<GLuint>(SIZE_MAX));
			gl(BindVertexArray, funcs)(pathBuffers[pbiPath].vao);
			gl(DrawArrays, funcs)(GL_LINE_STRIP, 0, num_path);
		}
		for(auto e: graph.edges()) {
			auto ep=EdgePriv::get(e);
			if(e.tree() && e.tree().selected()) {
				viewerShared->progs[P_EDGE]->setUniformValue("umpp", 2*umpp);
			} else {
				viewerShared->progs[P_EDGE]->setUniformValue("umpp", umpp);
			}
			if(colorMode) {
				if(e.tree()) {
					if(e.tree().index()<colormap.size()) {
						viewerShared->progs[P_EDGE]->setUniformValue("color_edge[0]", colormap[e.tree().index()]);
					} else {
						viewerShared->progs[P_EDGE]->setUniformValue("color_edge[0]", colors[C_TYPE_0+e.tree().index()%8]);
					}
				} else {
					viewerShared->progs[P_EDGE]->setUniformValue("color_edge[0]", colors[C_TYPE_OTHER]);
				}
			} else {
				viewerShared->progs[P_EDGE]->setUniformValue("color_edge[0]", colors[C_TYPE_0+e.type()%5]);
			}
			gl(Uniform1ui, funcs)(idx_loc, static_cast<GLuint>(ep->index+1));
			gl(BindVertexArray, funcs)(pathBuffers[ep->vaoi].vao);
			gl(DrawArrays, funcs)(GL_LINE_STRIP, 0, ep->points.size());
		}
		gl(Enable, funcs)(GL_BLEND);
	}
	void paintOpaque() {
		gl(BindFramebuffer, funcs)(GL_FRAMEBUFFER, fbo_opaque);
		gl(Viewport, funcs)(0, 0, fbo_width, fbo_height);
		gl(Clear, funcs)(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT|GL_STENCIL_BUFFER_BIT);

		float umpp;
		if(scale_factor<0) {
			umpp=radU*(1-scale_factor)/fbo_width;
		} else {
			umpp=radU/fbo_width;
		}


		viewerShared->progs[P_LINE]->bind();
		viewerShared->progs[P_LINE]->setUniformValue("mView", mView);
		viewerShared->progs[P_LINE]->setUniformValue("mProj", mProj);
		auto locp=viewerShared->progs[P_LINE]->uniformLocation("p0int");
		viewerShared->progs[P_LINE]->setUniformValue("umpp", umpp);
		auto& p=curPos.point;
		gl(Uniform3i, funcs)(locp, p._x, p._y, p._z);
		gl(BindVertexArray, funcs)(pathBuffers[pbiFixed].vao);
		viewerShared->progs[P_LINE]->setUniformValue("color", colors[C_AXIS]);
		/*
		for(int i=0; i<9; i++) {
			if(cubes[i].data) {
				// if shown?
				gl(DrawArrays, funcs)(GL_LINES, 2+i*24, 24);
			}
		}
		*/

		viewerShared->progs[P_LINE]->setUniformValue("umpp", umpp);
		/*
		if(!pickA.isNull() && !pickB.isNull()) {
			gl(PointSize, funcs)(3);
			//funcs->glDisable(GL_LINE_SMOOTH);
			viewerShared->progs[P_LINE]->setUniformValue("color", colors[C_ATTENTION]);
			gl(DrawArrays, funcs)(GL_LINES, 0, 2);
		}
		*/

		viewerShared->progs[P_POINT]->bind();
		viewerShared->progs[P_POINT]->setUniformValue("mView", mView);
		viewerShared->progs[P_POINT]->setUniformValue("mProj", mProj);
		viewerShared->progs[P_POINT]->setUniformValue("umpp", umpp/2);
		locp=viewerShared->progs[P_POINT]->uniformLocation("p0int");
		gl(Uniform3i, funcs)(locp, p._x, p._y, p._z);
		if(num_progr) {
			// dots showing computing progress
			gl(BindVertexArray, funcs)(vao_progr);
			viewerShared->progs[P_POINT]->setUniformValue("color", colors[C_ATTENTION]);
			gl(PointSize, funcs)(1);
			gl(DrawArrays, funcs)(GL_POINTS, 0, num_progr);
		}

		gl(BindVertexArray, funcs)(vao_fixed);
		// axis labels
		viewerShared->progs[P_MARK]->bind();
		viewerShared->progs[P_MARK]->setUniformValue("mView", mView);
		viewerShared->progs[P_MARK]->setUniformValue("mProj", mProj);
		viewerShared->progs[P_MARK]->setUniformValue("umpp", 3*umpp);
		viewerShared->progs[P_MARK]->bind();
		locp=viewerShared->progs[P_MARK]->uniformLocation("p0int");
		gl(Uniform3i, funcs)(locp, p._x, p._y, p._z);
		auto cent_loc=viewerShared->progs[P_MARK]->uniformLocation("center");

		for(auto n: graph.trees()) {
			if(colorMode) {
				if(n.index()<colormap.size()) {
					viewerShared->progs[P_MARK]->setUniformValue("color", colormap[n.index()]);
				} else {
					viewerShared->progs[P_MARK]->setUniformValue("color", colors[C_TYPE_0+n.index()%8]);
				}
			} else {
				viewerShared->progs[P_MARK]->setUniformValue("color", colors[C_TYPE_1]);
			}
			auto& p=n.root().point();
			//gl(Uniform4i, funcs)(cent_loc, p._x, p._y, p._z, p._r+3*radU);
			gl(Uniform4i, funcs)(cent_loc, p._x, p._y, p._z, 1);
			gl(DrawArrays, funcs)(GL_LINE_LOOP, 100, 64);
			if(n.selected()) {
				gl(DrawArrays, funcs)(GL_LINES, 44, 16);
			}
			// Neurons, in circle, mark
		}
		viewerShared->progs[P_MARK]->setUniformValue("umpp", umpp);
		viewerShared->progs[P_MARK]->setUniformValue("color", colors[C_AXIS]);
		/*
		for(int i=0; i<9; i++) {
			if(cubes[i].data) {
				double x0=cubes[i].xform.origin[0]+cubes[i].xform.direction[0+0*3]*cubes[i].data.cubeId().x0+cubes[i].xform.direction[0+1*3]*cubes[i].data.cubeId().y0+cubes[i].xform.direction[0+2*3]*cubes[i].data.cubeId().z0;
				double y0=cubes[i].xform.origin[1]+cubes[i].xform.direction[1+0*3]*cubes[i].data.cubeId().x0+cubes[i].xform.direction[1+1*3]*cubes[i].data.cubeId().y0+cubes[i].xform.direction[1+2*3]*cubes[i].data.cubeId().z0;
				double z0=cubes[i].xform.origin[2]+cubes[i].xform.direction[2+0*3]*cubes[i].data.cubeId().x0+cubes[i].xform.direction[2+1*3]*cubes[i].data.cubeId().y0+cubes[i].xform.direction[2+2*3]*cubes[i].data.cubeId().z0;
				auto dx=cubes[i].data.width();
				auto dy=cubes[i].data.height();
				auto dz=cubes[i].data.depth();
				{
					int dir=0;
					Point p{x0+cubes[i].xform.direction[0+dir*3]*dx,y0+cubes[i].xform.direction[1+dir*3]*dx,z0+cubes[i].xform.direction[2+dir*3]*dx};
					gl(Uniform4i, funcs)(cent_loc, p._x, p._y, p._z, 8);
					gl(DrawArrays, funcs)(GL_LINES, 18, 4);
				}
				{
					int dir=1;
					Point p{x0+cubes[i].xform.direction[0+dir*3]*dy,y0+cubes[i].xform.direction[1+dir*3]*dy,z0+cubes[i].xform.direction[2+dir*3]*dy};
					gl(Uniform4i, funcs)(cent_loc, p._x, p._y, p._z, 8);
					gl(DrawArrays, funcs)(GL_LINES, 22, 4);
				}
				{
					int dir=2;
					Point p{x0+cubes[i].xform.direction[0+dir*3]*dz,y0+cubes[i].xform.direction[1+dir*3]*dz,z0+cubes[i].xform.direction[2+dir*3]*dz};
					gl(Uniform4i, funcs)(cent_loc, p._x, p._y, p._z, 8);
					gl(DrawArrays, funcs)(GL_LINES, 26, 6);
				}
			}
		}
		*/


		/*
		if(tgtPos.point.valid()) {
			viewerShared->progs[P_MARK]->setUniformValue("color", colors[C_ATTENTION]);
			gl(Uniform4i, funcs)(cent_loc, tgtPos.point._x, tgtPos.point._y, tgtPos.point._z, 15);
			if(tgtPos.edge) {
				gl(DrawArrays, funcs)(GL_LINES, 10, 8);
			} else {
				gl(DrawArrays, funcs)(GL_LINES, 6, 4);
			}
			// Neurons, in circle, mark
		}
		if(curPos.point.valid()) {
			viewerShared->progs[P_MARK]->setUniformValue("color", colors[C_ATTENTION]);
			gl(Uniform4i, funcs)(cent_loc, curPos.point._x, curPos.point._y, curPos.point._z, 11);
			gl(DrawArrays, funcs)(GL_LINE_LOOP, 100, 64);
			// Neurons, in circle, mark
		}
		*/

		/* vertices */
		viewerShared->progs[P_MARK]->setUniformValue("color", colors[C_ALERT]);
		for(auto v: graph.vertices()) {
			auto& p=v.point();
			if(!v.finished()) {
				gl(Uniform4i, funcs)(cent_loc, p._x, p._y, p._z, 12);
				gl(DrawArrays, funcs)(GL_LINES, 40, 4);
			}
		}

		viewerShared->progs[P_SHADE]->bind();
		viewerShared->progs[P_SHADE]->setUniformValue("mView", mView);
		viewerShared->progs[P_SHADE]->setUniformValue("mProj", mProj);
		viewerShared->progs[P_SHADE]->setUniformValue("umpp", 2*umpp);
		locp=viewerShared->progs[P_SHADE]->uniformLocation("p0int");
		gl(Uniform3i, funcs)(locp, p._x, p._y, p._z);

		for(auto e: graph.edges()) {
			size_t a=0;
			size_t b=0;
			bool red=false;
			//
			if(e.inLoop()) {
				a=0;
				b=e.points().size();
				red=true;
			}
			if(e==tgtPos.edge) {
				if(e==curPos.edge) {
					a=curPos.index;
					b=tgtPos.index-a+1;
					if(tgtPos.index<a) {
						a=tgtPos.index;
						b=curPos.index-a+1;
					}
				} else {
					a=0;
					b=e.points().size();
				}
			}
			if(b>0) {
				auto ep=EdgePriv::get(e);

				if(red) {
					viewerShared->progs[P_SHADE]->setUniformValue("color_shade", colors[C_ALERT]);
				} else {
					viewerShared->progs[P_SHADE]->setUniformValue("color_shade", colors[C_ATTENTION]);
				}
				gl(BindVertexArray, funcs)(pathBuffers[ep->vaoi].vao);
				gl(DrawArrays, funcs)(GL_LINE_STRIP, a, b);
			}
		}
	}
	void paintSurface() {
		gl(BindFramebuffer, funcs)(GL_FRAMEBUFFER, fbo_surface);
		gl(Viewport, funcs)(0, 0, fbo_width, fbo_height);
		gl(Clear, funcs)(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT|GL_STENCIL_BUFFER_BIT);
		gl(Disable, funcs)(GL_BLEND);

		QColor color{255, 255, 255, 120};
		viewerShared->progs[P_MESH]->bind();
		viewerShared->progs[P_MESH]->setUniformValue("mView", mView);
		viewerShared->progs[P_MESH]->setUniformValue("mProj", mProj);
		viewerShared->progs[P_MESH]->setUniformValue("color", color);
		auto locp=viewerShared->progs[P_MESH]->uniformLocation("p0int");
		auto& p=curPos.point;
		gl(Uniform3i, funcs)(locp, p._x, p._y, p._z);

		gl(BindVertexArray, funcs)(vao_mesh);
		gl(PolygonMode, funcs)(GL_FRONT_AND_BACK, GL_FILL);


		gl(BindBuffer, funcs)(GL_ELEMENT_ARRAY_BUFFER, vbo_mesh[1]);
		gl(DrawElements, funcs)(GL_TRIANGLES, meshIdxes.size(), GL_UNSIGNED_INT, nullptr);
		gl(Enable, funcs)(GL_BLEND);
	}
	void paintVolume() {
		gl(Enable, funcs)(GL_STENCIL_TEST);
		gl(StencilFunc, funcs)(GL_NOTEQUAL, 1, 0xff);
		gl(StencilOp, funcs)(GL_KEEP, GL_KEEP, GL_REPLACE);
		gl(ActiveTexture, funcs)(GL_TEXTURE1);

		viewerShared->progs[P_VOLUME]->bind();
		viewerShared->progs[P_VOLUME]->setUniformValue("mrView", mrView);
		viewerShared->progs[P_VOLUME]->setUniformValue("mrProj", mrProj);
		gl(BindVertexArray, funcs)(vao_fixed);

		for(int ch=0; ch<3; ch++) {
			gl(BindFramebuffer, funcs)(GL_FRAMEBUFFER, fbo_cubes[ch]);
			gl(Viewport, funcs)(0, 0, fbo_width, fbo_height);
			gl(Clear, funcs)(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT|GL_STENCIL_BUFFER_BIT);
			viewerShared->progs[P_VOLUME]->setUniformValue("color_volume", colors[C_CHAN0+ch]);

			for(int layer=0; layer<3; layer++) {
				if(cube_textures[ch*3+layer]) {
					gl(BindTexture, funcs)(GL_TEXTURE_3D, viewerShared->cubeTextures[cube_textures[ch*3+layer]].tex);
					QVector2D xfunc(xfuncs[ch*3+layer].first, xfuncs[ch*3+layer].second);
					viewerShared->progs[P_VOLUME]->setUniformValue("xfunc_cube", xfunc);
					viewerShared->progs[P_VOLUME]->setUniformValue("mrTex", cube_matrices[ch*3+layer]);
					auto mvs=min_voxel_sizes[ch*3+layer];
					if(slice_mode) {
						viewerShared->progs[P_VOLUME]->setUniformValue("zparsCube", QVector3D(
									-1.0*slice_delta/slice_num,
									-1.0*slice_delta/slice_num,
									qMax(1.0/slice_num, 9*mvs/radU/8)));
					} else {
						viewerShared->progs[P_VOLUME]->setUniformValue("zparsCube", QVector3D(
									-1.0*slice_shown/slice_num,
									1.0*slice_shown/slice_num,
									qMax(1.0/slice_num, 9*mvs/radU/8)));
					}
					gl(DrawArrays, funcs)(GL_TRIANGLE_STRIP, 0, 4);
				}
			}
		}
		gl(Disable, funcs)(GL_STENCIL_TEST);
	}
	void paintBlank() {
		int width, height;
		if(scale_factor<0) {
			width=fbo_width/(1-scale_factor);
			height=fbo_height/(1-scale_factor);
		} else {
			width=fbo_width*(1+scale_factor);
			height=fbo_height*(1+scale_factor);
		}
		gl(Viewport, funcs)(0, 0, width, height);
		gl(Clear, funcs)(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT|GL_STENCIL_BUFFER_BIT);
	}
	void paintSorted(GLuint fbo) {
		gl(BindFramebuffer, funcs)(GL_DRAW_FRAMEBUFFER, fbo);
		gl(Viewport, funcs)(0, 0, fbo_width, fbo_height);
		gl(Clear, funcs)(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT|GL_STENCIL_BUFFER_BIT);

		gl(BindVertexArray, funcs)(vao_fixed);
		viewerShared->progs[P_SORT]->bind();
		gl(DrawArrays, funcs)(GL_TRIANGLE_STRIP, 0, 4);
	}

	void paintFinish(GLuint fbo) {
		bool scale=scale_factor!=0;
		paintSorted(scale?fbo_scale:fbo);
		if(scale) {
			gl(BindFramebuffer, funcs)(GL_READ_FRAMEBUFFER, fbo_scale);
			gl(BindFramebuffer, funcs)(GL_DRAW_FRAMEBUFFER, fbo);
			int width, height;
			if(scale_factor<0) {
				width=fbo_width/(1-scale_factor);
				height=fbo_height/(1-scale_factor);
			} else {
				width=fbo_width*(1+scale_factor);
				height=fbo_height*(1+scale_factor);
			}
			gl(Viewport, funcs)(0, 0, width, height);
			gl(BlitFramebuffer, funcs)(0, 0, fbo_width, fbo_height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_LINEAR);
		}
	}

	void loadSurface(const QString& fn) {
		QFile f{fn};
		if(!f.open(QIODevice::ReadOnly))
			throwError("Failed to open");
		QTextStream fs{&f};

		meshVerts.clear();
		meshIdxes.clear();
		QRegExp regex{"\\s+"};
		while(true) {
			auto line=fs.readLine();
			if(line.isNull()) {
				if(fs.atEnd())
					break;
				throwError("Failed to read line");
			}
			if(line.isEmpty())
				continue;
			if(line[0]=='#')
				continue;
			if(line.startsWith("mtllib "))
				continue;
			if(line.startsWith("o "))
				continue;
			if(line.startsWith("vn "))
				continue;
			if(line.startsWith("usemtl "))
				continue;
			if(line.startsWith("s "))
				continue;
			if(line.startsWith("v ")) {
				auto verts=line.mid(2).split(regex, QString::SkipEmptyParts);
				if(verts.size()!=3)
					throwError("Wrong number of coords: ", line);
				double v[3];
				for(int i=0; i<3; i++) {
					bool ok;
					v[i]=verts[i].toDouble(&ok);
					if(!ok)
						throwError("Cannot get double: ", line);
				}
				Point p{v[0], v[1], v[2]};
				meshVerts.push_back(MeshVert{p._x, p._y, p._z, 0.0, 0.0, 0.0});
			} else if(line.startsWith("f ")) {
				auto verts=line.mid(2).split(regex, QString::SkipEmptyParts);
				if(verts.size()!=3)
					throwError("Wrong number of coords: ", line);
				for(int i=0; i<3; i++) {
					auto vinfos=verts[i].split("//", QString::SkipEmptyParts);
					if(vinfos.size()<=0)
						throwError("No vert index: ", line);
					bool ok;
					auto vi=vinfos[0].toUInt(&ok);
					if(!ok)
						throwError("Cannot get uint: ", line);
					meshIdxes.push_back(vi-1);
				}
			} else {
				throwError("Unknown line: ", line);
			}
		}
		printMessage("nv: ", meshVerts.size(), "; ni: ", meshIdxes.size());
		if(!meshVerts.empty() && !meshIdxes.empty()) {

			std::vector<QVector3D> vertNormals(meshVerts.size(), {0.0f, 0.0f, 0.0f});
			for(size_t i=0; i+2<meshIdxes.size(); i+=3) {
				auto a=meshIdxes[i];
				auto b=meshIdxes[i+1];
				auto c=meshIdxes[i+2];
				QVector3D u(meshVerts[b].x-meshVerts[a].x, meshVerts[b].y-meshVerts[a].y, meshVerts[b].z-meshVerts[a].z);
				QVector3D v(meshVerts[c].x-meshVerts[a].x, meshVerts[c].y-meshVerts[a].y, meshVerts[c].z-meshVerts[a].z);
				QVector3D w(meshVerts[c].x-meshVerts[b].x, meshVerts[c].y-meshVerts[b].y, meshVerts[c].z-meshVerts[b].z);

				auto na=QVector3D::crossProduct(v, u);
				if(QVector3D::dotProduct(na, vertNormals[a])>=0) {
					vertNormals[a]+=na;
				} else {
					vertNormals[a]-=na;
				}
				auto nb=QVector3D::crossProduct(-u, w);
				if(QVector3D::dotProduct(nb, vertNormals[b])>=0) {
					vertNormals[b]+=nb;
				} else {
					vertNormals[b]-=nb;
				}
				auto nc=QVector3D::crossProduct(w, v);
				if(QVector3D::dotProduct(nc, vertNormals[c])>=0) {
					vertNormals[c]+=nc;
				} else {
					vertNormals[c]-=nc;
				}
			}
			for(size_t i=0; i<meshVerts.size(); i++) {
				auto n=vertNormals[i].length();
				meshVerts[i].nx=vertNormals[i].x()/n;
				meshVerts[i].ny=vertNormals[i].y()/n;
				meshVerts[i].nz=vertNormals[i].z()/n;
			}
			//
			//
			//
			//

			viewer->makeCurrent();
			gl(BindBuffer, funcs)(GL_ARRAY_BUFFER, vbo_mesh[0]);
			gl(BufferData, funcs)(GL_ARRAY_BUFFER, sizeof(MeshVert)*meshVerts.size(), &meshVerts[0], GL_STATIC_DRAW);
			gl(BindBuffer, funcs)(GL_ELEMENT_ARRAY_BUFFER, vbo_mesh[1]);
			gl(BufferData, funcs)(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLuint)*meshIdxes.size(), &meshIdxes[0], GL_STATIC_DRAW);
			viewer->doneCurrent();
		}
	}
	void loadColormap(const QString& fn) {
		QFile f{fn};
		if(!f.open(QIODevice::ReadOnly))
			throwError("Failed to open");
		QTextStream fs{&f};
		std::vector<QColor> cols;
		while(true) {
			auto line=fs.readLine();
			if(line.isNull()) {
				if(fs.atEnd())
					break;
				throwError("Failed to read line");
			}
			if(line.isEmpty())
				continue;
			//if(line[0]=='#')
			//continue;
			QColor col{line};
			if(!col.isValid())
				throwError("Error color: ", line);
			cols.push_back(col);
		}
		std::swap(colormap, cols);
	}
	void rotationTimeout() {
		QVector3D a(0, 0, -1);
		QVector3D b(.02, 0, -1);
		a=(mrView*mrProj*a);
		a.normalize();
		b=(mrView*mrProj*b);
		b.normalize();
		QVector3D norm{1.0, 0, 0};//=QVector3D::crossProduct(a, b);
		QMatrix4x4 mat{};
		float proj=QVector3D::dotProduct(a, b);
		if(proj<-1) proj=-1;
		if(proj>1) proj=1;
		float r=360*acos(proj)/2/M_PI;
		mat.rotate(-r, norm);
		up=mat*up;
		rgt=mat*rgt;
		updateMVPMatrices();
		viewer->update();
	}
	void toggleRotate() {
		if(timer->isActive()) {
			timer->stop();
		} else {
			timer->start();
		}
	}
};

Viewer::Viewer(Session* s):
	QOpenGLWidget{s, 0},
	priv{new ViewerPriv{s, this}}
{
	printMessage("Viewer new: ", priv->session, ", ", this);
	setMinimumSize(QSize(480, 320));
	setFocusPolicy(Qt::WheelFocus);
#ifdef DEBUG_FPS
	QTimer *timer;
	timer=new QTimer{this};
	timer->start(10);
	connect(timer, &QTimer::timeout, this, static_cast<void (Viewer::*)()>(&Viewer::repaint));
#endif

	setAcceptDrops(true);
}
Viewer::~Viewer() {
	printMessage("Viewer destroy: ", priv->session, ", ", this);
	delete priv;
}

void Viewer::setSession(Options* options, Graph gr, const std::array<Cube, 9>& cubes, const std::array<std::pair<double, double>, 9>& xfuncs, const std::array<double, 6>& bbox, double mvol) {
	priv->setSession(options, gr, cubes, xfuncs, bbox, mvol);
}

void Viewer::initializeGL() {
	bool fail=false;
	try {
		priv->initializeGL();
	} catch(const std::exception& e) {
		showWarning("Failed to initializeGL", e.what(), priv->session);
		fail=true;
	}
	priv->session->viewerReady(fail);
}

void Viewer::paintGL() {
	if(priv->graph) {
		priv->paintEdge();
		priv->paintOpaque();
		priv->paintSurface();
		priv->paintVolume();
		priv->paintFinish(defaultFramebufferObject());
	} else {
		priv->paintBlank();
	}
#ifdef DEBUG_FPS
	gl(Finish, priv->funcs)();
	printFPS();
#endif
}

void Viewer::resizeGL(int w, int h) {
	auto dpr=devicePixelRatio();
	int ww=dpr*w;
	int hh=dpr*h;
	return priv->resizeFrameBuffers(ww, hh);
}

void Viewer::wheelEvent(QWheelEvent* event) {
	if(priv->graph) {
		float d=event->angleDelta().y()/120.0;
		priv->zoomInOut(d);
	}
}

void Viewer::mousePressEvent(QMouseEvent* ev) {
	auto x=ev->x();
	auto y=ev->y();
	priv->mouse.xPressed=x;
	priv->mouse.yPressed=y;
	priv->mouse.x=x;
	priv->mouse.y=y;
	priv->mouse.moved=false;
}

void Viewer::mouseReleaseEvent(QMouseEvent* ev) {
	auto x=ev->x();
	auto y=ev->y();
	if(priv->mouse.x!=x || priv->mouse.y!=y) {
		priv->mouse.x=x;
		priv->mouse.y=y;
		priv->mouse.moved=true;
	}
	if(!priv->mouse.moved) {
		switch(ev->button()) {
			case Qt::LeftButton:
				if(priv->graph) {
					auto dpr=devicePixelRatio();
					if(priv->pickPoint(x*dpr, y*dpr)) {
						priv->session->updateActions();
						Tracer::instance()->logMessage("STAT: ", "EDIT_PICK_POS");
					}
					update();
				}
				break;
			case Qt::RightButton:
				priv->session->showPopupMenu(ev->globalPos());
				break;
			default:
				break;
		}
	}
}
void Viewer::mouseMoveEvent(QMouseEvent* ev) {
	auto x=ev->x();
	auto y=ev->y();
	if(priv->mouse.x!=x || priv->mouse.y!=y) {
		auto old_x=priv->mouse.x;
		auto old_y=priv->mouse.y;
		priv->mouse.x=x;
		priv->mouse.y=y;
		priv->mouse.moved=true;
		if(priv->graph && ev->buttons()==Qt::LeftButton) {
			QVector3D a((old_x*2+1.0)/width()-1, 1-(old_y*2+1.0)/height(), -1);
			QVector3D b((x*2+1.0)/width()-1,1-(y*2+1.0)/height(), -1);
			a=(priv->mrView*priv->mrProj*a);
			a.normalize();
			b=(priv->mrView*priv->mrProj*b);
			b.normalize();
			QVector3D norm=QVector3D::crossProduct(a, b);
			QMatrix4x4 mat{};
			float proj=QVector3D::dotProduct(a, b);
			if(proj<-1) proj=-1;
			if(proj>1) proj=1;
			float r=360*acos(proj)/2/M_PI;
			mat.rotate(-r, norm);
			priv->up=mat*priv->up;
			priv->rgt=mat*priv->rgt;
			priv->updateMVPMatrices();
			update();
		}
	}
}
bool Viewer::clearSelection() {
	priv->pickA=priv->pickB=QVector3D();
	return true;
}
void Viewer::updateCube(int ch, const Cube& cube, const std::pair<double, double>& xfunc) {
	// min voxel size
	makeCurrent();
	if(priv->cube_textures[ch]) {
		priv->viewerShared->releaseTexture(priv->funcs, priv->cube_textures[ch]);
		priv->cube_textures[ch]=0;
	}
	priv->cubes[ch]=cube;
	priv->xfuncs[ch]=xfunc;
	if(cube.data) {
		priv->cube_textures[ch]=priv->viewerShared->getTexture(priv->funcs, priv->cubes[ch].data);
		gl(BindBuffer, priv->funcs)(GL_ARRAY_BUFFER, priv->pathBuffers[priv->pbiFixed].vbo);

		auto ptr=static_cast<PointGL*>(priv->funcs->glMapBufferRange(GL_ARRAY_BUFFER, (2+ch*24)*sizeof(PointGL), 24*sizeof(PointGL), GL_MAP_WRITE_BIT|GL_MAP_INVALIDATE_RANGE_BIT));
		priv->setAxes(cube, ptr);
		gl(UnmapBuffer, priv->funcs)(GL_ARRAY_BUFFER);
		priv->updateCubeMatrix(ch);
		priv->updateVoxelSizes(ch);
	}
	doneCurrent();
}
void Viewer::updatePosition(const Position& pos, bool cur) {
	if(cur) {
		priv->curPos=pos;
		for(int i=0; i<9; i++) {
			if(priv->cubes[i].data)
				priv->updateCubeMatrix(i);
		}
	} else {
		priv->tgtPos=pos;
	}
}
void Viewer::updateEdgeNode(EdgePriv* ep, size_t edgPos) {
	if(ep->vaoi<=0) {
		return updateEdge(ep);
	}
	makeCurrent();
	gl(BindBuffer, priv->funcs)(GL_ARRAY_BUFFER, priv->pathBuffers[ep->vaoi].vbo);
	auto ptr=static_cast<PointGL*>(priv->funcs->glMapBufferRange(GL_ARRAY_BUFFER, edgPos*sizeof(PointGL), 1*sizeof(PointGL), GL_MAP_WRITE_BIT));
	ptr->r=ep->points[edgPos]._r;
	ptr->m=ep->points[edgPos].m;
	gl(UnmapBuffer, priv->funcs)(GL_ARRAY_BUFFER);
	doneCurrent();
}
void Viewer::recycleEdge(EdgePriv* ep) {
	if(ep->vaoi<=0)
		return;
	priv->freePbi(ep->vaoi);
	ep->vaoi=-1;
}
void Viewer::updateEdge(EdgePriv* ep) {
	makeCurrent();
	if(ep->vaoi<=0)
		ep->vaoi=priv->createPbi();
	auto pathGL=priv->pathToPathGL(ep->points);
	gl(BindBuffer, priv->funcs)(GL_ARRAY_BUFFER, priv->pathBuffers[ep->vaoi].vbo);
	gl(BufferData, priv->funcs)(GL_ARRAY_BUFFER, sizeof(PointGL)*pathGL.size(), pathGL.data(), GL_STATIC_DRAW);
	doneCurrent();
}
void Viewer::setProgressData(const std::vector<int32_t>& dat) {
	priv->num_progr=dat.size()/3;
	if(!priv->num_progr)
		return;
	std::vector<GLint> pos(dat.size());
	for(size_t j=0; j<dat.size(); j++) {
		pos[j]=dat[j];
	}
	makeCurrent();
	gl(BindBuffer, priv->funcs)(GL_ARRAY_BUFFER, priv->vbo_progr);
	gl(BufferData, priv->funcs)(GL_ARRAY_BUFFER, pos.size()*sizeof(GLint), pos.data(), GL_STATIC_DRAW);
	doneCurrent();
}
void Viewer::setPathData(const Path& pth) {
	if(pth.points.size()<2) {
		priv->num_path=0;
		return;
	}
	priv->num_path=pth.points.size();
	auto pathGL=priv->pathToPathGL(pth.points);
	makeCurrent();
	gl(BindBuffer, priv->funcs)(GL_ARRAY_BUFFER, priv->pathBuffers[priv->pbiPath].vbo);
	gl(BufferData, priv->funcs)(GL_ARRAY_BUFFER, sizeof(PointGL)*pathGL.size(), pathGL.data(), GL_STATIC_DRAW);
	doneCurrent();
}
void Viewer::saveOptions(Options* options) const {
	options->setXyz("viewer.direction.up", priv->up.x(), priv->up.y(), priv->up.z());
	options->setXyz("viewer.direction.right", priv->rgt.x(), priv->rgt.y(), priv->rgt.z());
	options->setDouble("viewer.zoom", priv->radU);
	ViewerGeneralOptions::instance()->saveConfig(priv, options);
	ViewerColorOptions::instance()->saveConfig(priv, options);
	ViewerVolumeOptions::instance()->saveConfig(priv, options);
}

void Viewer::attachOptionsDlg() {
	ViewerGeneralOptions::instance()->attach(priv);
	ViewerColorOptions::instance()->attach(priv);
	ViewerVolumeOptions::instance()->attach(priv);
}
void Viewer::detachOptionsDlg() {
	ViewerGeneralOptions::instance()->attach(nullptr);
	ViewerColorOptions::instance()->attach(nullptr);
	ViewerVolumeOptions::instance()->attach(nullptr);
}

void Viewer::updateXfunc(int ch, const std::pair<double, double>& xfunc) {
	priv->updateXfunc(ch, xfunc);
}
void Viewer::updateBoundingBox(const std::array<double, 6>& bbox) {
	priv->updateBoundingBox(bbox);
}

void Viewer::dropEvent(QDropEvent* drop) {
	auto data=drop->mimeData();
	if(data->hasFormat("text/uri-list")) {
		auto urls=data->urls();
		if(urls.size()==1 && urls[0].isLocalFile()) {
			auto file=urls[0].toLocalFile();
			if(file.endsWith(QLatin1String{".txt"}, Qt::CaseInsensitive)) {
				priv->loadColormap(file);
				update();
			} else if(file.endsWith(QLatin1String{".obj"}, Qt::CaseInsensitive)) {
				priv->loadSurface(file);
				update();
			}
		}
	}
	/*
		if(urls.size()==1) {
		QFile f{urls[0].toLocalFile()};
		if(!f.open(QIODevice::ReadOnly))
		throwError("Failed to open");
		QTextStream fs{&f};
		int group;
		std::vector<int> groups{};
		while(true) {
		fs>>group;
		auto stat=fs.status();
		if(stat!=QTextStream::Ok) {
		if(stat==QTextStream::ReadPastEnd)
		break;
		throwError("Failed to read line");
		}
		printMessage("COLOR ", group);
		groups.push_back(group);
		}
		std::swap(priv->colormap, groups);
		for(auto i: groups) {
		printMessage("COLOR ", i);
		}
		update();
		}
		*/
}
void Viewer::dragEnterEvent(QDragEnterEvent* drop) {
	if(drop->mimeData()->hasFormat("text/uri-list")) {
		auto urls=drop->mimeData()->urls();
		if(urls.size()==1 && urls[0].isLocalFile()) {
			auto file=urls[0].toLocalFile();
			if(file.endsWith(QLatin1String{".txt"}, Qt::CaseInsensitive)
					|| file.endsWith(QLatin1String{".obj"}, Qt::CaseInsensitive)) {
				drop->accept();
			}
		}
	}
}

QImage Viewer::takeScreenshot() {
	if(!priv->graph)
		return {};

	makeCurrent();
	priv->paintEdge();
	priv->paintOpaque();
	priv->paintSurface();
	priv->paintVolume();
	priv->paintSorted(priv->fbo_scale);
	gl(Finish, priv->funcs)();

	QImage img{priv->fbo_width, priv->fbo_height, QImage::Format_ARGB32};
	gl(BindFramebuffer, priv->funcs)(GL_READ_FRAMEBUFFER, priv->fbo_scale);
	gl(ReadBuffer, priv->funcs)(GL_COLOR_ATTACHMENT0);
	gl(ReadPixels, priv->funcs)(0, 0, priv->fbo_width, priv->fbo_height, GL_BGRA, GL_UNSIGNED_BYTE, img.bits());
	doneCurrent();
	return img.mirrored(false, true);
}

void Viewer::toggleRotate() {
	priv->toggleRotate();
}
void Viewer::setSliceMode(bool checked) {
	if(priv->graph) {
		priv->slice_mode=checked;
		priv->slice_delta=0;
		update();
		return;
	}
}
void Viewer::setEdgeVisibility(bool checked) {
	if(priv->graph) {
		priv->show_edges=checked;
		update();
		return;
	}
}

#include "viewer.options.cc"

